﻿using System;
using System.ComponentModel;
using System.Linq;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
using System.Collections.Generic;
using System.Text;

namespace Argos.Scheduling
{
	/// <summary>
	/// The base class for all schedule classes.
	/// </summary>
	[Serializable]
	public abstract class Schedule : IEquatable<Schedule>
	{

		#region #ctor: protected Schedule()

		/// <summary>
		/// Creates a new instance of the class.
		/// </summary>
		protected Schedule() { }

		#endregion



		#region public static Schedule Always { get; }

		/// <summary>
		/// Returns a schedule that always has a current occurrence.
		/// </summary>
		public static Schedule Always
		{
			get
			{
				CustomSchedule s = new CustomSchedule();
				s.AddOccurrence(new Occurrence(DateTime.MinValue, DateTime.MaxValue));
				return s;
			}
		}

		#endregion

		#region public static Schedule Never { get; }

		/// <summary>
		/// Returns a schedule that never has a current occurrence.
		/// </summary>
		public static Schedule Never
		{
			get { return new CustomSchedule(); }
		}

		#endregion
		
		#region public static Schedule OfficeHours { get; }

		/// <summary>
		/// Returns a schedule that represents office hours, i.e. from 08:00 to 16:00
		/// on monday thru friday.
		/// </summary>
		public static Schedule OfficeHours
		{
			get
			{
				WeeklySchedule schedule = new WeeklySchedule();
				WeekdayList list = WeekdayList.WorkDays;

				foreach (DayOfWeek day in list)
				{
					schedule.Entries.Add(new WeeklySchedule.Entry(day, new TimeOfDay(8), TimeSpan.FromHours(8)));
				}

				return schedule;
			}
		}

		#endregion



		#region public Occurrence CurrentOccurrence { get; }

		/// <summary>
		/// Returns the currently active occurrence that is defined in the schedule.
		/// </summary>
		/// <remarks>
		/// If the schedule does not define an occurrence that is currently active,
		/// this property returns null.
		/// </remarks>
		public Occurrence CurrentOccurrence
		{
			get { return this.GetOccurrence(DateTime.Now); }
		}

		#endregion

		#region public ScheduleEntryList Entries { get; }

		private ScheduleEntryList _Entries;

		/// <summary>
		/// Returns the entries in the schedule that define the occurrences
		/// of the schedule.
		/// </summary>
		public ScheduleEntryList Entries
		{
			get
			{
				if (null == _Entries)
				{
					_Entries = new ScheduleEntryList();
				}
				return _Entries;
			}
		}

		#endregion		
		


		#region public void AddDateTime(DateTime start)

		/// <summary>
		/// Adds a new entry to the schedule using the given date and time with a zero duration.
		/// </summary>
		/// <param name="start">The date and time that defines the start of the new entry.</param>
		public void AddDateTime(DateTime start)
		{
			this.AddDateTime(start, TimeSpan.Zero);
		}

		#endregion

		#region public virtual void AddDateTime(DateTime start, TimeSpan duration)

		/// <summary>
		/// When implemented in a class, adds a new entry to the schedule using the given
		/// date and time and the given duration.
		/// </summary>
		/// <param name="start">The date and time that defines the start of the new entry.</param>
		/// <param name="duration">The duration for the entry.</param>
		public virtual void AddDateTime(DateTime start, TimeSpan duration) { }

		#endregion

		#region public override bool Equals(object obj)

		/// <summary>
		/// Compares the current schedule to the given object.
		/// </summary>
		/// <param name="obj">The object to compare to the current.</param>
		public override bool Equals(object obj)
		{
			if (obj is Schedule)
			{
				return this.Equals((Schedule)obj);
			}

			return false;
		}

		#endregion

		#region public bool Equals(Schedule other)

		/// <summary>
		/// Compares the current schedule instance to the given schedule.
		/// </summary>
		/// <param name="other">The schedule to compare to the current.</param>
		public bool Equals(Schedule other)
		{
			if (null == other)
			{
				return false;
			}

			if (this.Entries.Count != other.Entries.Count)
			{
				return false;
			}

			for (int i = 0; i < this.Entries.Count; i++)
			{
				if (this.Entries[i] != other.Entries[i])
				{
					return false;
				}
			}

			return true;
		}

		#endregion
		
		#region public override int GetHashCode()

		/// <summary>
		/// Returns the hash code for the schedule.
		/// </summary>
		public override int GetHashCode()
		{
			return this.ToString().GetHashCode();
		}

		#endregion
		
		#region public Occurrence GetNextOccurrence()

		/// <summary>
		/// Returns the next occurrence from the schedule.
		/// </summary>
		public Occurrence GetNextOccurrence()
		{
			return this.GetNextOccurrence(DateTime.Now);
		}

		#endregion

		#region public Occurrence GetNextOccurrence(DateTime after)

		/// <summary>
		/// Returns the next occurrence from the schedule that starts after the given
		/// date and time.
		/// </summary>
		/// <param name="after">The date and time from which to return the next occurrence.</param>
		public Occurrence GetNextOccurrence(DateTime after)
		{
			List<Occurrence> list = new List<Occurrence>();
			foreach (ScheduleEntry entry in this.Entries)
			{
				Occurrence o = entry.GetNextOccurrence(after);
				if (null != o)
				{
					list.Add(o);
				}
			}

			if (list.Count > 0)
			{
				list.Sort();
				return list.First();
			}

			return null;
		}

		#endregion

		#region public Occurrence GetOccurrence(DateTime at)

		/// <summary>
		/// Returns the occurrence from the schedule that is active at the given date and time.
		/// </summary>
		/// <param name="at">The date and time for which to return the active occurrence.</param>
		/// <returns>
		/// Returns the occurrence active at the given date and time. Returns null if there is
		/// no active occurrence at the given date and time.
		/// </returns>
		public Occurrence GetOccurrence(DateTime at)
		{
			Occurrence o = this.GetPreviousOccurrence(at);
			
			if (null != o && o.Start.ToUniversalTime() < at.ToUniversalTime() && o.End.ToUniversalTime() > at.ToUniversalTime())
			{
				// If the occurrence started before the given date and time and
				// ended after the given date and time, it means that the occurrence
				// was or is active at the given date and time, so we return it.
				return o;
			}

			return null;
		}

		#endregion
		
		#region public Occurrence GetPreviousOccurrence()

		/// <summary>
		/// </summary>
		public Occurrence GetPreviousOccurrence()
		{
			return this.GetPreviousOccurrence(DateTime.Now);
		}

		#endregion

		#region public Occurrence GetPreviousOccurrence(DateTime before)

		/// <summary>
		/// </summary>
		/// <param name="before">The date and time for which to return the start of the previous occurrence.</param>
		public Occurrence GetPreviousOccurrence(DateTime before)
		{
			List<Occurrence> list = new List<Occurrence>();
			foreach (ScheduleEntry entry in this.Entries)
			{
				Occurrence o = entry.GetPreviousOccurrence(before);
				if (null != o)
				{
					list.Add(o);
				}
			}

			if (list.Count > 0)
			{
				list.Sort();
				return list.Last();
			}

			return null;
		}

		#endregion

		#region public byte[] ToByteArray()

		/// <summary>
		/// Returns the schedule serialized to a byte array.
		/// </summary>
		public byte[] ToByteArray()
		{
			using (MemoryStream strm = new MemoryStream())
			{
				this.Write(strm);
				return strm.ToArray();
			}
		}

		#endregion

		#region public override string ToString()

		/// <summary>
		/// Returns the string representation of the schedule.
		/// </summary>
		public override string ToString()
		{
			StringBuilder sb = new StringBuilder();
			int entryCount = 5;

			for (int i = 0; i < Math.Min(entryCount, this.Entries.Count); i++)
			{
				if (i > 0)
				{
					sb.Append(", ");
				}
				sb.Append(this.Entries[i]);
			}

			if (this.Entries.Count > entryCount)
			{
				sb.AppendFormat(" ... ({0} entries)", this.Entries.Count);
			}

			if (this.Entries.Count == 0)
			{
				sb.Append("0 entries");
			}

			return sb.ToString();
		}

		#endregion
		
		#region public void Write(Stream stream)

		/// <summary>
		/// Writes the current instance to the given stream.
		/// </summary>
		/// <param name="stream">The stream to write the current instance to.</param>
		public void Write(Stream stream)
		{
			BinaryFormatter formatter = new BinaryFormatter();
			formatter.Serialize(stream, this);
		}

		#endregion



		#region public static Schedule Read(Stream stream)

		/// <summary>
		/// Reads the schedule from the given stream and returns it.
		/// </summary>
		/// <param name="stream">The stream to read the schedule from.</param>
		public static Schedule Read(Stream stream)
		{
			return Schedule.Read<Schedule>(stream);
		}

		#endregion

		#region public static Schedule Read(byte[] buffer)

		/// <summary>
		/// Reads the schedule from the given buffer and returns it.
		/// </summary>
		/// <param name="buffer">The buffer to read the schedule from.</param>
		public static Schedule Read(byte[] buffer)
		{
			return Schedule.Read<Schedule>(buffer);
		}

		#endregion
		

		
		#region protected static T Read<T>(Stream stream)

		/// <summary>
		/// Reads the schedule from the given stream and returns it as
		/// the given type.
		/// </summary>
		/// <typeparam name="T">The type to return the read schedule as.</typeparam>
		/// <param name="stream">The stream to read the schedule from.</param>
		protected static T Read<T>(Stream stream) where T : Schedule
		{
			Schedule s;

			BinaryFormatter formatter = new BinaryFormatter();
			s = (Schedule)formatter.Deserialize(stream);

			return (T)s;
		}

		#endregion

		#region protected static T Read<T>(byte[] buffer) where T : Schedule

		/// <summary>
		/// Reads the schedule from the given buffer and returns it as
		/// the given type.
		/// </summary>
		/// <typeparam name="T">The type to return the schedule as.</typeparam>
		/// <param name="buffer">The buffer to read the schedule from.</param>
		protected static T Read<T>(byte[] buffer) where T : Schedule
		{
			using (MemoryStream strm = new MemoryStream())
			{
				strm.Write(buffer, 0, buffer.Length);
				strm.Position = 0;

				return Schedule.Read<T>(strm);
			}
		}

		#endregion

	}
}
